//
// Created by lymk on 2022/2/26.
//

#include "RenderPipeline.h"
#include "Shader/PhongShader.h"
#include "Shader/LambertShader.h"
using namespace std;

void RenderPipeline::DrawObject(const Object& object){
    IMesh* mesh = object.mesh;
    vector<VertexIn> vertexList;
    vector<vector<int>> idxList;
    mesh->GetVertex(vertexList,idxList);

    shader->uniform->model = object.GetModelMatrix();
    shader->uniform->viewPort = camera->GetViewPortMatrix();
    shader->uniform->perspective = camera->GetPerspectiveMatrix();

    for(int i=0;i<idxList.size();i++){
        DrawTriangle(i,idxList[i],vertexList);
    }
}

void RenderPipeline::DrawTriangle(int triIdx,const std::vector<int>& idx, const std::vector<VertexIn>& vertexList) {
    auto PerspectiveDivision = [](glm::vec4 p)->glm::vec4 {
        return {p.x/p.w,p.y/p.w,p.z,p.w};
    };
    vector<VertexOut> FragIn;
    for(int i=0;i<idx.size();i++){
        FragIn.push_back(shader->VertexShader(vertexList[idx[i]]));
        FragIn[i].pos = PerspectiveDivision(FragIn[i].pos);
        FragIn[i].worldPos /= FragIn[i].worldPos.w;
    }

    glm::vec2 topLeft = {-1.f,1.f};
    float stepW = 2.f/screenWeight;
    float stepH = 2.f/screenHeight;

    auto worldToScreen = [&](vector<glm::vec4> origin){
        vector<glm::vec2> res;
        res.emplace_back((origin[0].x+1.f)/stepW,(1.0-origin[0].y)/stepH);
        res.emplace_back((origin[1].x+1.f)/stepW,(1.0-origin[1].y)/stepH);
        res.emplace_back((origin[2].x+1.f)/stepW,(1.0-origin[2].y)/stepH);
        return res;
    };
    vector<glm::vec4> originTriangle = {FragIn[0].pos,FragIn[1].pos,FragIn[2].pos};
    vector<glm::vec4> worldTriangle = {FragIn[0].worldPos,FragIn[1].worldPos,FragIn[2].worldPos};
    vector<glm::vec2> screenTriangle = worldToScreen(originTriangle);
    vector<glm::vec3> triangleNormal = {FragIn[0].normal,FragIn[1].normal,FragIn[2].normal};
    vector<glm::vec4> triangleColor = {FragIn[0].color,FragIn[1].color,FragIn[2].color};
    vector<glm::vec2> triangleTexCoord = {FragIn[0].texCoord,FragIn[1].texCoord,FragIn[2].texCoord};

    int iBottom= min(float(screenHeight),max(screenTriangle[0].y,max(screenTriangle[1].y,screenTriangle[2].y)));
    int iTop = max(0.f,min(screenTriangle[0].y,min(screenTriangle[1].y,screenTriangle[2].y)));
    int iRight = min(float(screenWeight),max(screenTriangle[0].x,max(screenTriangle[1].x,screenTriangle[2].x)));
    int iLeft = max(0.f,min(screenTriangle[0].x,min(screenTriangle[1].x,screenTriangle[2].x)));
    for(int i=iTop;i<=iBottom;i++){
        for(int j=iLeft;j<=iRight;j++){
            if(InsideTriangle({j,i},screenTriangle)){
                //float coordY = topLeft.y - (i*stepH);
                //float coordX = topLeft.x + (j*stepW);
                //glm::vec3 barycentricCoord = ComputeBarycentric({coordX,coordY},originTriangle);

                glm::vec3 barycentricCoord = ComputeBarycentric({j,i},screenTriangle);
                /*float Z = barycentricCoord[0] * originTriangle[0].z / originTriangle[0].w +
                        barycentricCoord[1] * originTriangle[1].z / originTriangle[1].w+
                        barycentricCoord[2] * originTriangle[2].z / originTriangle[2].w;
                Z = Z / (barycentricCoord[0] / originTriangle[0].w +
                        barycentricCoord[1] / originTriangle[1].w +
                        barycentricCoord[2] / originTriangle[2].w);*/
                float Z = GetZBuffer(barycentricCoord,originTriangle[0].w,originTriangle[1].w,originTriangle[2].w);

                if(Z < frameBuffer->depthBuffer->getDepth(j,i)) {
                    //cout<<Z<<" ";
                    frameBuffer->depthBuffer->setDepth(j, i, Z);
                    VertexOut tmpOut;
                    glm::vec4 ZBuffer = {originTriangle[0].w,originTriangle[1].w,originTriangle[2].w,Z};
                    tmpOut.worldPos = Interpolation(barycentricCoord,ZBuffer, worldTriangle);
                    tmpOut.normal = Interpolation(barycentricCoord,ZBuffer, triangleNormal);
                    tmpOut.color = Interpolation(barycentricCoord,ZBuffer, triangleColor);
                    //tmpOut.texCoord = Interpolation(barycentricCoord,ZBuffer, triangleTexCoord);
                    glm::vec4 pixelColor = shader->FragmentShader(tmpOut);
                    frameBuffer->colorBuffer->SetElement(j, i, {static_cast<unsigned char>(255.999*pixelColor.x),
                                                                static_cast<unsigned char>(255.999*pixelColor.y),
                                                                static_cast<unsigned char>(255.999*pixelColor.z),
                                                                static_cast<unsigned char>(255.999*pixelColor.w)});
                }
            }
        }
    }
}

Buffer *RenderPipeline::GetRenderBuffer() {
    return frameBuffer->colorBuffer;
}

void RenderPipeline::Tick(GameTimer *timer) {
    for(auto& item : world){
        DrawObject(item);
    }
    float deltaSecond = timer->DeltaTime();
    world[0].RotateY(fmod(deltaSecond*30,360));
    world[1].RotateX(fmod(deltaSecond*30,360));
    /*
    for(auto& item : world){
        item.RotateX(fmod(deltaSecond*90,360));
        item.RotateY(fmod(deltaSecond*90,360));
        item.RotateZ(fmod(deltaSecond*90,360));
    }*/
}

bool RenderPipeline::InsideTriangle(glm::vec2 pos, const vector<glm::vec2> &triangle) {
    glm::vec2 A = {triangle[0].x,triangle[0].y};
    glm::vec2 B = {triangle[1].x,triangle[1].y};
    glm::vec2 C = {triangle[2].x,triangle[2].y};

    glm::vec2 BC = C-B;
    glm::vec2 BA = A-B;

    glm::vec2 AC = C-A;
    glm::vec2 AB = B-A;

    glm::vec2 BP = pos - B;
    glm::vec2 AP = pos - A;

    auto crossVec2 = [](glm::vec2 v1,glm::vec2 v2){
        return v1.x*v2.y - v2.x*v1.y;
    };
    //cout<<crossVec2(AC,AP)<<' '<<crossVec2(AC,AB)<<endl;
    bool b0 = !(crossVec2(BC,BA)>=0 ^ crossVec2(BC,BP) >= 0);
    bool b1 = !(crossVec2(AC,AP)>=0 ^ crossVec2(AC,AB) >= 0);
    bool b2 = !(crossVec2(AB,AC)>=0 ^ crossVec2(AB,AP) >= 0);

    return b1&b2&b0;
}

glm::vec3 RenderPipeline::ComputeBarycentric(glm::vec2 pos,const vector<glm::vec2> &triangle) {
    glm::vec2 P1 = {triangle[0].x,triangle[0].y};
    glm::vec2 P2 = {triangle[1].x,triangle[1].y};
    glm::vec2 P3 = {triangle[2].x,triangle[2].y};

    auto crossVec2 = [](glm::vec2 v1,glm::vec2 v2){
        return v1.x*v2.y - v2.x*v1.y;
    };

    glm::vec2 A = P1 - P3;
    glm::vec2 B = P2 - P3;
    glm::vec2 C = pos - P3;

    glm::vec3 res;
    res.x = crossVec2(C,B)/crossVec2(A,B);
    res.y = crossVec2(C,A)/crossVec2(B,A);
    res.z = 1-res.x-res.y;

    return res;
}

glm::vec3 RenderPipeline::Interpolation(glm::vec3 coord,glm::vec4 ZBuffer, const vector<glm::vec3> &value) {
    coord.x/=ZBuffer[0];
    coord.y/=ZBuffer[1];
    coord.z/=ZBuffer[2];
    glm::vec3 res = glm::vec3();
    res.x = ZBuffer[3] *((value[0].x*coord.x) + (value[1].x*coord.y) + (value[2].x*coord.z));
    res.y = ZBuffer[3] *((value[0].y*coord.x) + (value[1].y*coord.y) + (value[2].y*coord.z));
    res.z = ZBuffer[3] *((value[0].z*coord.x) + (value[1].z*coord.y) + (value[2].z*coord.z));
    return res;
}

glm::vec2 RenderPipeline::Interpolation(glm::vec3 coord,glm::vec4 ZBuffer, const vector<glm::vec2> &value) {
    coord.x/=ZBuffer[0];
    coord.y/=ZBuffer[1];
    coord.z/=ZBuffer[2];
    glm::vec2 res = glm::vec2();
    res.x = ZBuffer[3] *((value[0].x*coord.x) + (value[1].x*coord.y) + (value[2].x*coord.z));
    res.y = ZBuffer[3] *((value[0].y*coord.x) + (value[1].y*coord.y) + (value[2].y*coord.z));
    return res;
}

glm::vec4 RenderPipeline::Interpolation(glm::vec3 coord,glm::vec4 ZBuffer, const vector<glm::vec4> &value) {
    coord.x/=ZBuffer[0];
    coord.y/=ZBuffer[1];
    coord.z/=ZBuffer[2];
    glm::vec4 res = glm::vec4();
    res.x = ZBuffer[3] *((value[0].x*coord.x) + (value[1].x*coord.y) + (value[2].x*coord.z));
    res.y = ZBuffer[3] *((value[0].y*coord.x) + (value[1].y*coord.y) + (value[2].y*coord.z));
    res.z = ZBuffer[3] *((value[0].z*coord.x) + (value[1].z*coord.y) + (value[2].z*coord.z));
    res.w = ZBuffer[3] *((value[0].w*coord.x) + (value[1].w*coord.y) + (value[2].w*coord.z));
    return res;
}

float RenderPipeline::GetZBuffer(glm::vec3 coord, float Z1, float Z2, float Z3) {
    float res = coord.x*(1.f/Z1) + coord.y*(1.f/Z2) + coord.z*(1.f/Z3);
    return 1.f/res;
}

void RenderPipeline::Initialize(int w,int h) {
    screenWeight = w;
    screenHeight = h;
    frameBuffer = new FrameBuffer;
    frameBuffer->colorBuffer = new Buffer(w,h,4);
    frameBuffer->depthBuffer = new DepthBuffer(w,h);
    camera = new Camera();
    shader = new PhongShader();
}

void RenderPipeline::AddObject(Object o) {
    world.push_back(o);
}

void RenderPipeline::AddPointLight(Light l) {
    shader->uniform->lights.push_back(l);
}

void RenderPipeline::Clear() {
    frameBuffer->colorBuffer->Clear();
    frameBuffer->depthBuffer->Clear();
}


glm::mat4 Object::GetModelMatrix() const {
    return transformMatrix;
}

Object::Object(IMesh *m) {
    mesh = m;
    transformMatrix = glm::mat4(
            1,0,0,0,
            0,1,0,0,
            0,0,1,0,
            0,0,0,1
            );
}

void Object::Translate(glm::vec3 distance) {
    transformMatrix[3][0] = distance.x;
    transformMatrix[3][1] = distance.y;
    transformMatrix[3][2] = distance.z;
}

void Object::RotateX(float degree) {
    float PI = 3.1415926535898;
    float hudu = degree * (PI / 180.f);
    /*
    glm::mat4 tmp = {
        cos(hudu),-sin(hudu),0,0,
        sin(hudu),cos(hudu),0,0,
        0,0,1,0,
        0,0,0,1
    };*/
    glm::mat4 tmp = {
            1,0,0,0,
            0,cos(hudu),-sin(hudu),0,
            0,sin(hudu),cos(hudu),0,
            0,0,0,1
    };
    transformMatrix = tmp * transformMatrix;
}

void Object::Scale(glm::vec3 s) {
    transformMatrix[0][0] *= s.x;
    transformMatrix[1][1] *= s.y;
    transformMatrix[2][2] *= s.z;
}

void Object::RotateY(float degree) {
    float PI = 3.1415926535898;
    float hudu = degree * (PI / 180.f);
    glm::mat4 tmp = {
            cos(hudu),0,sin(hudu),0,
            0,1,0,0,
            -sin(hudu),0,cos(hudu),0,
            0,0,0,1
    };
    transformMatrix = tmp * transformMatrix;
}

void Object::RotateZ(float degree) {
    float PI = 3.1415926535898;
    float hudu = degree * (PI / 180.f);
    glm::mat4 tmp = {
        cos(hudu),-sin(hudu),0,0,
        sin(hudu),cos(hudu),0,0,
        0,0,1,0,
        0,0,0,1
    };
    transformMatrix = tmp * transformMatrix;
}
